On this page

Skip to content

Generating Excel with Watermarks using .NET

TLDR

  • Excel does not have a built-in watermark feature; it can be simulated by setting a "full-page transparent background image."
  • Watermark display positions: Background is suitable for Normal and Page Layout views; Header is suitable for Page Layout view and printing.
  • When generating a watermark image, you must set the pixel dimensions according to the Excel PaperSize configuration.
  • When using NPOI, due to the lack of a direct API, you must implement a custom VmlDrawing class and manipulate the underlying XML structure.
  • It is recommended to use SkiaSharp instead of System.Drawing.Common in modern .NET environments to resolve cross-platform support issues.

Principles and Limitations of Simulating Watermarks

Excel does not provide a native watermark feature; developers must achieve this by setting a full-page transparent background image. Depending on the requirements, there are two ways to configure this:

  • Background Setting: Suitable for "Normal View" and "Page Layout View."
  • Header Setting: Suitable for "Page Layout View" and "Printing."

Currently, there is no effective method for setting watermarks in "Page Break Preview."

Generating Full-Page Watermark Images

Before generating the image, you must obtain the paper size (PaperSize) from the Excel page setup. If the paper orientation is landscape, you need to swap the width and height values.

Obtaining Paper Dimensions

You can obtain the system's default paper specifications via PrinterSettings:

csharp
PrinterSettings settings = new PrinterSettings() {
    PrinterName = "Microsoft XPS Document Writer"
};
foreach (System.Drawing.Printing.PaperSize printerPaperSize in settings.PaperSizes) {
    // Get RawKind, PaperName, Width, Height
}

Image Scaling and Drawing

If the image dimensions do not match the page, you need to scale and center it:

csharp
public Image ResizeImageBackgroundToFullPage(Image watermark, int width, int height){
    if (watermark.Width > width || watermark.Height > height) {
        using (Image image = ZoomOutImage(width, height)) {
            return ResizeImageBackgroundToFullPageInternal(width, height, image);
        }
    }
    return ResizeImageBackgroundToFullPageInternal(width, height, watermark);
}

private Image ResizeImageBackgroundToFullPageInternal(int pageWidth, int pageHeight, Image image) {
    Image bitmap = new Bitmap(pageWidth, pageHeight);
    using Graphics graphics = Graphics.FromImage(bitmap);
    graphics.Clear(Color.White);
    graphics.DrawImage(image, (pageWidth - image.Width) / 2, (pageHeight - image.Height) / 2);
    graphics.CompositingQuality = CompositingQuality.HighQuality;
    graphics.SmoothingMode = SmoothingMode.HighQuality;
    graphics.Save();
    return bitmap;
}

Generating Text Watermarks

If you need to convert text into an image, you can use Graphics to rotate and draw it:

csharp
public Image DrawText(string text, Font font, Color textColor, Color backColor, int width, int height) {
    Image img = new Bitmap(width, height);
    using (Graphics drawing = Graphics.FromImage(img)) {
        SizeF textSize = drawing.MeasureString(text, font, 0, StringFormat.GenericTypographic);
        float x = (width - textSize.Width) / 2;
        float y = (height - textSize.Height) / 2;

        drawing.TranslateTransform(x + (textSize.Width / 2), y + (textSize.Height / 2));
        drawing.RotateTransform(-45);
        drawing.TranslateTransform(-(x + (textSize.Width / 2)), -(y + (textSize.Height / 2)));

        drawing.Clear(backColor);
        Brush textBrush = new SolidBrush(textColor);
        drawing.DrawString(text, font, textBrush, x, y);
        drawing.Save();
        return img;
    }
}

Generating Watermarks Using EPPlus

In older versions of EPPlus, you could set it directly via HeaderFooter and BackgroundImage:

csharp
sheet.HeaderFooter.OddHeader.InsertPicture(watermark, PictureAlignment.Centered);
sheet.BackgroundImage.Image = watermark;

WARNING

EPPlus 6 and later versions have removed the dependency on System.Drawing.Common. The code above may no longer work; it is recommended to consult the latest API documentation.

Generating Watermarks Using NPOI (XLSX)

NPOI does not have an API for setting background images directly; you must implement a custom VmlDrawing class and manipulate the underlying XML structure.

Custom VmlDrawing Class

You need to inherit from POIXMLDocumentPart and implement the XML writing logic:

csharp
private class VmlDrawing : POIXMLDocumentPart {
    public string PictureRelId { get; set; }
    public Image Image { get; set; }

    protected override void Commit() {
        PackagePart part = GetPackagePart();
        Stream @out = part.GetOutputStream();
        Write(@out);
        @out.Close();
    }

    private void Write(Stream stream) {
        float width = Image.Width * 72 / Image.HorizontalResolution;
        float height = Image.Height * 72 / Image.VerticalResolution;

        using StreamWriter sw = new(stream);
        XmlDocument doc = new();
        doc.LoadXml($@"
<xml xmlns:v=""urn:schemas-microsoft-com:vml"" xmlns:o=""urn:schemas-microsoft-com:office:office"" xmlns:x=""urn:schemas-microsoft-com:office:excel"">
<v:shape id=""CH"" type=""#_x0000_t75"" style=""position:absolute;margin-left:0;margin-top:0;width:{width}pt;height:{height}pt;z-index:1"">
<v:imagedata o:relid=""{PictureRelId}"" o:title="""" />
<o:lock v:ext=""edit"" rotation=""t"" />
</v:shape>
</xml>");
        doc.Save(stream);
    }
}

Setting the Watermark

Add the image to the Workbook and associate it with the Sheet:

csharp
MemoryStream imageMs = new MemoryStream();
watermark.Save(imageMs, System.Drawing.Imaging.ImageFormat.Png);

int pictureIdx = workbook.AddPicture(imageMs.ToArray(), PictureType.PNG);
POIXMLDocumentPart docPart = workbook.GetAllPictures()[pictureIdx] as POIXMLDocumentPart;
POIXMLDocumentPart.RelationPart backgroundRelPart = sheet.AddRelation(null, XSSFRelation.IMAGES, docPart);

sheet.GetCTWorksheet().picture = new CT_SheetBackgroundPicture {
    id = backgroundRelPart.Relationship.Id
};

// Create VmlDrawing association
VmlDrawing drawing = (VmlDrawing)sheet.CreateRelationship(VmlRelation.Instance, XSSFFactory.GetInstance(), drawingNumber);
POIXMLDocumentPart.RelationPart headerRelPart = drawing.AddRelation(null, XSSFRelation.IMAGES, docPart);
drawing.Image = watermark;
drawing.PictureRelId = headerRelPart.Relationship.Id;

sheet.Header.Center = HeaderFooter.PICTURE_FIELD.sequence;
sheet.GetCTWorksheet().legacyDrawingHF = new CT_LegacyDrawing {
    id = sheet.GetRelationId(drawing)
};

Changelog

  • 2023-02-24 Initial document created.
  • 2026-05-17 Added link to GitHub sample project.